<?php

namespace App\Lib\Rooms\Services;

use App\Constants\Action;
use App\Constants\Gang;
use App\Constants\Rooms;
use App\Lib\_Error\ExecuteException;
use App\Lib\_Error\ThirdScripts\CanNotGangException;
use App\Lib\_Error\ThirdScripts\CanNotPengException;
use App\Lib\_Error\ThirdScripts\NotCardException;
use App\Lib\_Error\ThirdScripts\NotDiscardCardException;
use App\Lib\_Error\ValidateException;
use App\Lib\LibBaseService;
use App\Lib\Rooms\Events\ActionDiscardEvent;
use App\Lib\Rooms\Events\ActionDrawEvent;
use App\Lib\Rooms\Events\ActionGangEvent;
use App\Lib\Rooms\Events\ActionPengEvent;
use App\Model\RoundActionModel;
use App\Model\RoundsModel;

class ActionService extends LibBaseService
{


    /**
     * 摸牌
     * @param RoundsModel $rounds
     * @param string $position
     * @param bool $isGang
     * @return RoundActionModel
     * @throws ExecuteException
     * @throws ValidateException
     */
    public function draw(RoundsModel $rounds, string $position = Rooms::BANKER, bool $isGang = false)
    {
        $action = $this->create($rounds, Action::DRAW, $position);

        /** @var array $cards */
        $remain_cards = $rounds->remain_cards;
        if (!$remain_cards) {
            throw new ExecuteException('操作异常.');
        }
        if (empty($remain_cards)) {
            throw new ValidateException('牌局已结束');
        }
        if ($isGang) {
            $val = array_shift($remain_cards);
        } else {
            $val = array_pop($remain_cards);
        }
        $rounds->remain_cards = $remain_cards;

        $pos = $action->position;

        $cards = $action->{"{$pos}_cards"};
        array_push($cards, $val);
        $action->{"{$pos}_cards"} = $cards;
        $action->action_cards = self::getActionCardsByVal($val, Action::DRAW);

        if (!$action->save() || !$rounds->save()) {
            throw new ExecuteException('摸牌失败');
        }
        $this->eventDispatcher->dispatch(new ActionDrawEvent($action, $rounds, $val));

        return $action;
    }

    /**
     * 弃牌
     * @param RoundsModel $rounds
     * @param string $position
     * @return RoundActionModel
     * @throws ExecuteException
     * @throws NotCardException
     * @throws ValidateException
     */
    public function discard(RoundsModel $rounds, string $position = Rooms::BANKER)
    {
        $action = $this->create($rounds, Action::DISCARD, $position);

        /** @var int $val */
        $val = $action->getActionVal();
        if (is_null($rounds) || is_null($val)) {
            throw new ExecuteException('操作异常');
        }
        $pos = $action->position;

        $cards = $action->{"{$pos}_cards"};
        $index = array_search($val, $cards);
        if ($index === false) {
            throw new NotCardException('该玩家并无该牌');
        }
        array_splice($cards, $index, 1);
        $action->{"{$pos}_cards"} = $cards;

        $discard = $action->{"{$pos}_discard"} ?? [];
        array_push($discard, $val);
        $action->{"{$pos}_discard"} = $discard;
        $action->action_cards = self::getActionCardsByVal($val, Action::DISCARD);
        if (!$action->save() || !$rounds->fill($action->toArray())->save()) {
            throw new ExecuteException('出牌失败');
        }

        $this->eventDispatcher->dispatch(new ActionDiscardEvent($action, $rounds, $val));

        return $action;
    }

    /**
     * 碰牌
     * @param RoundsModel $rounds
     * @param string $fusePos 出牌玩家位置
     * @param string $position
     * @return RoundActionModel
     * @throws CanNotPengException
     * @throws ExecuteException
     * @throws NotDiscardCardException
     * @throws ValidateException
     */
    public function peng(RoundsModel $rounds, string $fusePos, string $position = Rooms::BANKER)
    {
        $action = $this->create($rounds, Action::PENG, $position);
        /** @var int $val */
        $val = $action->getActionVal();
        if (is_null($val)) {
            throw new ExecuteException('操作异常');
        }


        $fuse_discard = $action->{"{$fusePos}_discard"} ?? [];
        // 该位置玩家最后出的一张牌值
        $fuse_card = array_pop($fuse_discard);
        if (is_null($fuse_card) || $fuse_card !== $val) {
            throw new NotDiscardCardException('该牌值并非是前一次动作出的牌');
        }
        // 从被碰玩家的出牌中取出要碰的牌
        $action->{"{$fusePos}_discard"} = $fuse_discard;

        /** @var string $pos */
        $pos = $action->position;
        $cards = $action->{"{$pos}_cards"};
        $i = 0;
        foreach ($cards as $key => $item) {
            if ($item === $val) {
                $i++;
                unset($cards[$key]);
            }
            if ($i >= 2) {
                break;
            }
        }
        if ($i < 2) {
            throw new CanNotPengException('手牌中凑不足一对');
        }
        $cards = array_values($cards);
        $action->{"{$pos}_cards"} = $cards;


        $peng = $action->{"{$pos}_peng"} ?? [];
        array_push($peng, [
            'val' => $val, // 碰的牌
            'fuse_position' => $fusePos, // 被碰的玩家位置（以庄家为参照物）
            'fuse_uid' => $rounds->{"{$fusePos}_id"}, // 被碰的玩家ID
            'fuse_direction' => self::positionToDirection($pos, $fusePos), // 被碰的玩家方位（以当前操作员为参照物）
        ]);
        $action->{"{$pos}_peng"} = $peng;
        $action->action_cards = self::getActionCardsByVal($val, Action::PENG);

        if (!$action->save() || !$rounds->fill($action->toArray())->save()) {
            throw new ExecuteException('碰牌失败');
        }
        $this->eventDispatcher->dispatch(new ActionPengEvent($action, $rounds, $val));
        return $action;
    }

    /**
     * 杠牌
     * @param RoundsModel $rounds
     * @param string $fusePos 出牌玩家位置
     * @param int $type
     * @param string $position
     * @return RoundActionModel
     * @throws CanNotGangException
     * @throws ExecuteException
     * @throws NotCardException
     * @throws NotDiscardCardException
     * @throws ValidateException
     */
    public function gang(RoundsModel $rounds, string $fusePos, int $type, string $position = Rooms::BANKER)
    {
        $action = $this->create($rounds, Action::GANG, $position);
        if (!in_array($type, Gang::TYPES)) {
            throw new ValidateException('杠牌类型异常');
        }
        
        $val = $action->getActionVal();
        if (is_null($rounds) || is_null($val)) {
            throw new ExecuteException('操作异常');
        }
        /** @var string $pos */
        $pos = $action->position;
        $cards = $action->{"{$pos}_cards"};
        if ($type === Gang::TYPE_PENG) { // 碰杠
            $index = array_search($val, $cards);
            if ($index === false) {
                throw new NotCardException('无法杠牌');
            }

            $peng = $action->{"{$pos}_peng"} ?? [];
            if (empty($peng)) {
                throw new CanNotGangException('无法杠牌.');
            }
            $pengItem = [];
            foreach ($peng as $key => $item) {
                if ($item['val'] ?? null === $val) {
                    $pengItem = $item;
                    unset($cards[$key]);
                    break;
                }
            }
            if (empty($pengItem)) {
                throw new CanNotGangException('无法杠牌!');
            }
            // 从碰牌列表中移除玩家玩家点碰的牌
            $action->{"{$pos}_peng"} = array_values($peng);
            $fusePos = $pengItem['fuse_position'];
            // 移除手牌中需要杠的牌
            array_splice($cards, $index, 1);

        } elseif ($type === Gang::TYPE_AN) { // 暗杠
            $i = 0;
            foreach ($cards as $key => $item) {
                if ($item === $val) {
                    $i++;
                    unset($cards[$key]);
                }
                if ($i >= 4) {
                    break;
                }
            }
            if ($i < 4) {
                throw new CanNotGangException('手牌中凑不暗杠');
            }
            $cards = array_values($cards);
        } else { // 明杠
            $fuse_discard = $action->{"{$fusePos}_discard"} ?? [];
            // 该位置玩家最后出的一张牌值
            $fuse_card = array_pop($fuse_discard);
            if (is_null($fuse_card) || $fuse_card !== $val) {
                throw new NotDiscardCardException('该牌值并非是前一次动作出的牌');
            }
            // 从被碰玩家的出牌中取出要碰的牌
            $action->{"{$fusePos}_discard"} = $fuse_discard;

            $i = 0;
            foreach ($cards as $key => $item) {
                if ($item === $val) {
                    $i++;
                    unset($cards[$key]);
                }
                if ($i >= 3) {
                    break;
                }
            }
            if ($i < 3) {
                throw new CanNotGangException('手牌中凑不出杠');
            }
            $cards = array_values($cards);
        }

        // 重设手牌
        $action->{"{$pos}_cards"} = $cards;

        // 新增杠牌
        $gang = $action->{"{$pos}_gang"} ?? [];
        array_push($gang, [
            'val' => $val, // 杠的牌
            'fuse_position' => $fusePos, // 被杠的玩家位置（以庄家为参照物）
            'fuse_uid' => $rounds->{"{$fusePos}_id"}, // 被杠的玩家ID
            'fuse_direction' => self::positionToDirection($pos, $fusePos), // 被杠的玩家方位（以当前操作员为参照物）
            'type' => $type, // 类型：0：暗杠、1：明杠、2：碰后杠
        ]);
        $action->{"{$pos}_gang"} = $gang;
        $action->action_cards = self::getActionCardsByVal($val, Action::GANG);

        if (!$action->save() || !$rounds->fill($action->toArray())->save()) {
            throw new ExecuteException('杠牌失败');
        }

        $this->eventDispatcher->dispatch(new ActionGangEvent($action, $rounds, $val, $type));
        // 摸牌
        $this->draw($rounds, $position, true);
        return $action;
    }

    /**
     * 将以庄家为参照物的方位转换为以玩家为参照物的方位
     * @param string $currentPos
     * @param string $fusePos
     * @return null|string
     * @throws ValidateException
     */
    public static function positionToDirection(string $currentPos, string $fusePos)
    {
        $arr = [
            Rooms::BANKER => 0,
            Rooms::NEXT => 1,
            Rooms::OPPOSITE => 2,
            Rooms::PREV => 3,
        ];
        if (!isset($currentPos) || !isset($fusePos)) {
            throw new ValidateException('不存在该位置');
        }

        $pos = $arr[$fusePos] - $arr[$currentPos];
        switch ($pos) {
            case -3:
            case 1:
                return 'right';
            case 3:
            case -1:
                return 'left';
            case 2:
            case -2:
                return 'center';
        }
        return null;
    }

    /**
     * 将牌值转换为动作牌值组
     * @param int $val
     * @param string $action
     * @return array
     */
    public static function getActionCardsByVal(int $val, string $action): array
    {
        switch ($action) {
            case Action::DRAW:
            case Action::DISCARD:
            case Action::HU:
            case Action::WIN:
                return [$val];
            case Action::PENG:
                return array_pad([], 3, $val);
            case Action::GANG:
                return array_pad([], 4, $val);
        }
        return [];
    }

    /**
     * 生成动作
     * @param RoundsModel $rounds
     * @param string $action
     * @param string $position
     * @return RoundActionModel
     * @throws ExecuteException
     * @throws ValidateException
     */
    protected function create(
        RoundsModel $rounds,
        string $action,
        string $position = Rooms::BANKER
    )
    {
        $roundAction = new RoundActionModel([
            'rounds_id' => $rounds->id,
            'rooms_id' => $rounds->rooms_id,
            'uid' => $rounds->{"{$position}_id"},
            'position' => $position,
            'action' => $action,
        ]);
        if (method_exists($roundAction, $action)) {
            throw new ValidateException('不允许该操作');
        }
        if ($roundAction->fill($rounds->toArray())->save()) {
            throw new ExecuteException('创建动作失败');
        }
        return $roundAction;
    }

}